/*
 * Copyright (c) 2011, Andrew Cole
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

package net.andycole.xmiparser.model;

import java.util.HashMap;
import java.util.ArrayList;

import org.eclipse.uml2.uml.UMLFactory;
import org.eclipse.uml2.uml.Model;
import org.eclipse.uml2.uml.Class;
import org.eclipse.uml2.uml.Interface;
import org.eclipse.uml2.uml.Classifier;
import org.eclipse.uml2.uml.DataType;
import org.eclipse.uml2.uml.Package;
import org.eclipse.uml2.uml.Property;
import org.eclipse.uml2.uml.Parameter;
import org.eclipse.uml2.uml.Operation;
import org.eclipse.uml2.uml.Realization;
import org.eclipse.uml2.uml.Generalization;
import org.eclipse.uml2.uml.Comment;

import org.eclipse.uml2.uml.ParameterDirectionKind;
import org.eclipse.uml2.uml.VisibilityKind;

import net.andycole.xmiparser.transfer.TransferClass;
import net.andycole.xmiparser.transfer.TransferOperation;
import net.andycole.xmiparser.transfer.TransferParameter;
import net.andycole.xmiparser.transfer.TransferAttribute;
import net.andycole.xmiparser.transfer.ASTypes;


/**
 * Provides the ability to add content to a UML metamodel that is compatible
 * with that of Eclipse's EMF.
 * @author Andrew Cole
 */
public class EclipseModelImpl implements IModel
{
   private Model modelPackage;
   private UMLFactory factory;

   private HashMap<String, Class> classCache;
   private HashMap<String, Interface> interfaceCache;
   private HashMap<String, Package> packageCache;
   private HashMap<String, DataType> dataTypeCache;

   private boolean useMultiplicities = false;

   // There doesn't appear to be any type to hold documentation
   // for classes, methods or attributes

   private ArrayList<DeferredAttributeHolder> deferredAttributes;
   private ArrayList<DeferredParameterHolder> deferredParameters;

   public EclipseModelImpl()
   {
      super();
      init();
   }

   @Override
   public void setUseMultiplicities(boolean value)
   {
      useMultiplicities = value;
   }

   @Override
   public void addClass(TransferClass newClass)
   {
      String pkgName = newClass.getPackagePath();
      addClassByName(pkgName, newClass);
   }

   private void addClassByName(String pkgName,
                               TransferClass newClass)
   {
      String qualifiedClassName = newClass.getQualifiedClassName();
      Classifier cachedClass = classCache.get(qualifiedClassName);

      Package parentPkg = getParentPackage(pkgName);
      if (cachedClass == null)
      {
         // create/get parent packages
         // get parent package

         // create class ModelElement and set in cache
         if (newClass.isInterface())
         {
            cachedClass = factory.createInterface();
            interfaceCache.put(qualifiedClassName, (Interface)cachedClass);
         }
         else
         {
            cachedClass = factory.createClass();
            classCache.put(qualifiedClassName, (Class)cachedClass);
         }
         parentPkg.getPackagedElements().add(cachedClass);
      }

      cachedClass.setName(newClass.getClassName());

      // set class attributes/operations etc.
      if (newClass.isInterface())
      {
         for (TransferOperation method : newClass.getMethods())
         {
            addMethodToInterface(method, (Interface)cachedClass);
         }

         for (TransferAttribute attr : newClass.getAttributes())
         {
            addAttributeToInterface(attr, (Interface)cachedClass);
         }
      }
      else
      {
         for (TransferOperation method : newClass.getMethods())
         {
            addMethodToClass(method, (Class)cachedClass);
         }

         for (TransferAttribute attr : newClass.getAttributes())
         {
            addAttributeToClass(attr, (Class)cachedClass);
         }
      }

      setSuperclasses(newClass, cachedClass, parentPkg);

      if (newClass.getAnnotation() != null &&
          newClass.getAnnotation().length() > 0)
      {
         Comment comment = factory.createComment();
         cachedClass.getOwnedComments().add(comment);
         comment.setBody(newClass.getAnnotation());
      }
   }

   private void setSuperclasses(TransferClass newClass, Classifier cachedClass, Package parentPackage)
   {
      // extends (generalization)
      // get the parent class from the cache
      String baseClassName = newClass.getExtends();
      if (baseClassName != null && baseClassName.length() > 0)
      {
         Classifier baseClass = getClassByName(baseClassName,
                                               false);
         Generalization gens = factory.createGeneralization();
         gens.setGeneral(baseClass);
         gens.setSpecific(cachedClass);
      }

      // realization (abstraction)
      // iterate over list of implemented interfaces
      for (String interfaceClassName : newClass.getImplementsArray())
      {
         Classifier inter = getClassByName(interfaceClassName,
                                           true);
         if (!(inter instanceof Interface) ||
             !(cachedClass instanceof Class))
            throw new IllegalArgumentException("Could not set Realization. " +
                                               " Supplier or Client could not be found ");

         Realization realization = factory.createRealization();
         realization.getClients().add(cachedClass);
         realization.getSuppliers().add(inter);

         parentPackage.getPackagedElements().add(realization);
      }
   }

   private void init()
   {
      classCache = new HashMap<String, Class>();
      interfaceCache = new HashMap<String, Interface>();
      dataTypeCache = new HashMap<String, DataType>();

      packageCache = new HashMap<String, Package>();
      deferredAttributes = new ArrayList<DeferredAttributeHolder>();
      deferredParameters = new ArrayList<DeferredParameterHolder>();

      factory = UMLFactory.eINSTANCE;
      modelPackage = factory.createModel();
      modelPackage.setName("AS3Model");

      createPrimitives();
   }

   private void createPrimitives()
   {
      for (ASTypes type : ASTypes.values())
      {
         DataType dt = factory.createDataType();
         String typeName = type.toString();
         dt.setName(typeName);

         modelPackage.getPackagedElements().add(dt);
         dataTypeCache.put(typeName, dt);
      }

   }

   @Override
   public Object getBasePackage()
   {
      return modelPackage;
   }

   @Override
   public void startTransaction(boolean isWrite)
   {

   }

   @Override
   public void endTransaction()
   {
      for (DeferredAttributeHolder holder : deferredAttributes)
      {
         Classifier type = getClassByName(holder.qualifiedClassName, false);
         Property element = holder.element;
         element.setType(type);
      }
      for (DeferredParameterHolder holder : deferredParameters)
      {
         Classifier type = getClassByName(holder.qualifiedClassName, false);
         Parameter element = holder.element;
         element.setType(type);
      }
   }

   private Classifier getClassByName(String  qualClassName,
                                     boolean isInterface)
   {
      Classifier cachedClass = dataTypeCache.get(qualClassName);
      if (cachedClass == null)
         cachedClass = classCache.get(qualClassName);
      if (cachedClass == null)
         cachedClass = interfaceCache.get(qualClassName);

      if (cachedClass == null)
      {
         String className = qualClassName;
         String pkgName = "";
         int lastSepIndex = qualClassName.lastIndexOf(".");
         if (lastSepIndex > 0)
         {
            className = qualClassName.substring(lastSepIndex+1);
            pkgName = qualClassName.substring(0, lastSepIndex);
         }
         Package parentPkg = getParentPackage(pkgName);
         // create class ModelElement and set in cache
         if (isInterface)
         {
            cachedClass = factory.createInterface();
            interfaceCache.put(qualClassName, (Interface)cachedClass);
         }
         else
         {
            cachedClass = factory.createClass();
            classCache.put(qualClassName, (Class)cachedClass);
         }
         parentPkg.getPackagedElements().add(cachedClass);

         cachedClass.setName(className);
      }
      return cachedClass;
   }

   private void addMethodToInterface(TransferOperation tMethod, Interface cachedInterface)
   {
      Operation op = createMethod(tMethod);
      cachedInterface.getOwnedOperations().add(op);
   }

   private void addMethodToClass(TransferOperation tMethod, Class cachedClass)
   {
      Operation op = createMethod(tMethod);
      cachedClass.getOwnedOperations().add(op);
   }

   private Operation createMethod(TransferOperation tMethod)
   {
      Operation op = factory.createOperation();
      op.setName(tMethod.getName());

      for (TransferParameter tParam : tMethod.getParameters())
      {
         Parameter param = factory.createParameter();
         param.setName(tParam.getName());
         op.getOwnedParameters().add(param);
         deferredParameters.add(new DeferredParameterHolder(param, tParam.getType()));
      }

      op.setVisibility(getVisibilityKind(tMethod.getAccessLevel()));
      op.setIsStatic(tMethod.isStatic());

      String resultTypeName = tMethod.getResultType();
      if (resultTypeName != null && resultTypeName.length() > 0)
      {
         Parameter outParam = factory.createParameter();
         op.getOwnedParameters().add(outParam);
         //Classifier paramClass = getClassByName(resultTypeName,
         //                                       false);
         deferredParameters.add(new DeferredParameterHolder(outParam, resultTypeName));

         outParam.setDirection(ParameterDirectionKind.RETURN_LITERAL);
      }

      if (tMethod.getAnnotation() != null &&
          tMethod.getAnnotation().length() > 0)
      {
         Comment comment = factory.createComment();
         op.getOwnedComments().add(comment);
         comment.setBody(tMethod.getAnnotation());
      }

      return op;
   }

   private VisibilityKind getVisibilityKind(String visibility)
   {
      VisibilityKind retKind = VisibilityKind.PRIVATE_LITERAL;
      if ("private".equals(visibility))
         retKind = VisibilityKind.PRIVATE_LITERAL;
      else if ("protected".equals(visibility))
         retKind = VisibilityKind.PROTECTED_LITERAL;
      else if ("public".equals(visibility))
         retKind = VisibilityKind.PUBLIC_LITERAL;

      return retKind;
   }

   private void addAttributeToClass(TransferAttribute tAttribute,
                                    Class cachedClass)
   {
      Property att = factory.createProperty();
      att.setName(tAttribute.getName());
      if (tAttribute.getMetaType() != null && useMultiplicities)
      {
         att.setUpper(-1);
         deferredAttributes.add(new DeferredAttributeHolder(att, tAttribute.getMetaType()));
      }
      else
      {
         deferredAttributes.add(new DeferredAttributeHolder(att, tAttribute.getType()));
      }

      cachedClass.getOwnedAttributes().add(att);
      att.setIsReadOnly(tAttribute.isConst());
      att.setVisibility(getVisibilityKind(tAttribute.getAccessLevel()));
      att.setIsStatic(tAttribute.isStatic());

      if (tAttribute.getAnnotation() != null &&
          tAttribute.getAnnotation().length() > 0)
      {
         Comment comment = factory.createComment();
         att.getOwnedComments().add(comment);
         comment.setBody(tAttribute.getAnnotation());
      }
   }

   private void addAttributeToInterface(TransferAttribute tAttribute,
                                        Interface cachedClass)
   {
      Property att = factory.createProperty();
      att.setName(tAttribute.getName());
      if (tAttribute.getMetaType() != null && useMultiplicities)
      {
         att.setUpper(-1);
         deferredAttributes.add(new DeferredAttributeHolder(att, tAttribute.getMetaType()));
      }
      else
      {
         deferredAttributes.add(new DeferredAttributeHolder(att, tAttribute.getType()));
      }

      cachedClass.getOwnedAttributes().add(att);
      att.setIsReadOnly(tAttribute.isConst());
      att.setVisibility(getVisibilityKind(tAttribute.getAccessLevel()));
      att.setIsStatic(tAttribute.isStatic());

      if (tAttribute.getAnnotation() != null &&
          tAttribute.getAnnotation().length() > 0)
      {
         Comment comment = factory.createComment();
         att.getOwnedComments().add(comment);
         comment.setBody(tAttribute.getAnnotation());
      }
   }

   private Package getParentPackage(String pkgPath)
   {
      Package currentPackage = modelPackage;
      String[] pkgList = null;
      if (pkgPath.length() > 0)
      {
         pkgList = pkgPath.split("\\.");
         for (String pkgName : pkgList)
         {
            Package parentPkg = currentPackage;
            currentPackage = packageCache.get(pkgName);
            if (currentPackage == null)
            {
               currentPackage = factory.createPackage();
               currentPackage.setName(pkgName);
               parentPkg.getPackagedElements().add(currentPackage);
               packageCache.put(pkgName, currentPackage);
            }
         }
      }

      return currentPackage;
   }

   private class DeferredParameterHolder
   {
      public DeferredParameterHolder(Parameter element,
                                     String qualifiedClassName)
      {
         this.element = element;
         this.qualifiedClassName = qualifiedClassName;
      }

      public Parameter element;
      public String qualifiedClassName;
   }

   private class DeferredAttributeHolder
   {
      public DeferredAttributeHolder(Property element,
                                     String qualifiedClassName)
      {
         this.element = element;
         this.qualifiedClassName = qualifiedClassName;
      }
      public Property element;
      public String qualifiedClassName;
   }
}